Flutter RenderAndroidView

Virtual displays 模式下,在 Flutter 侧,开发者直接打交道的组件是 AndroidViewAndroidView 中通过层层封装,最终负责展示的组件 RenderAndroidView,它是一个 RenderObject,负责实际的嵌入 Android 视图展示。在本文中,我们将介绍 RenderAndroidView 的实现,了解了它,就了解了 Virtual displays 在 Flutter 侧和核心原理。上层的类,只是进行一些封装和通信功能。

paint 绘制

RenderAndroidView,继承自 RenderBox,是一个 RenderObject。RenderObject 的 paint 负责实际绘制。RenderAndroidView 的绘制方法如下:

@override
void paint(PaintingContext context, Offset offset) {
  // 必须要有纹理 id
  if (_viewController.textureId == null) return;
  // ...
  _paintTexture(context, offset);
}

其中,_viewController 我们在前面组成结构中提到过,你看都一层层传递到最底层了。它其中持有一个 id,这是原生视图的纹理 id。纹理 id 是什么呢?在本文中我们不深究它,简单理解为,只要原生侧那边工作正常,就会产生这个 id,而 Flutter 这边,只要拿到这个 id,即可进行后续绘制工作。(纹理 id 将在《Flutter Android Virtual Display 实现原理》的其他文章中介绍)。

_paintTexture

纹理 id 可以不讲,RenderAndroidView 怎么拿纹理 id 进行绘制的,这个还是要讲的。继续看 _paintTexture

void _paintTexture(PaintingContext context, Offset offset) {
  context.addLayer(TextureLayer(
    rect: offset & _currentAndroidViewSize,
    textureId: _viewController.textureId!,
    freeze: _state == _PlatformViewState.resizing,
  ));
}

首先,看到了纹理 id,它作为 TextureLayer 的构造函数,TextureLayer 是什么呢?文档中说到,这是一个将将后端纹理映射到矩形的合成层。也可以点击《Flutter TextureLayer》阅读我的笔记。

不难看出,我们将 textureId 对应的纹理,映射到 rect 对应的区域。rect 里面怎么还个 & 运算?这是 Offset 类重载的运算符,用于将 Rect 偏移 Offset 的距离。

还有个 freeze 是干什么的?一个布尔值,表示是否冻结纹理。冻结后纹理不再更新。不难看出 _state 是一个状态机,冻结条件是 RenderAndroidView 处于 resizing(缩放)状态,可以理解为在缩放过程中,纹理不更新,等待缩放过程完成后,再更新纹理,避免在视图大小改变时出现不必要的缩放效果。

下面再看 PaintingContext,PaintingContext 是 Flutter 中的一个关键类,用于管理和绘制图形。它作为绘图操作的上下文,提供了一系列方法来绘制在屏幕上显示的图形。

addLayer 方法用于在绘图过程中添加图层。图层是独立于主画布的绘制环境,可以用于实现各种视觉效果。如此看来,渲染纹理就要通过添加一个 TextureLayer 的层来实现

3 个 override get

在 RenderAndroidView 中,有 3 个 override 的 get 属性:

@override
bool get sizedByParent => true;

@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest;
}

@override
bool get alwaysNeedsCompositing => true;

@override
bool get isRepaintBoundary => true;

根据文档他们的含义分别是:

performResize

Resize(缩放)是 RenderAndroidView 中的一项核心操作,从前面我们看到 RenderAndroidView 是个状态机,专门有一个状态叫 _PlatformViewState.resizing 就能看出,事情不简单。

@override
void performResize() {
  super.performResize();
  _sizePlatformView();
}

这段代码调用了 _sizePlatformView,注意这是一个异步方法,它用于调整平台视图的大小。

// 这个属性存储了 Android 原生侧视图的大小
late Size _currentAndroidViewSize;

// 注意这是一个异步方法
Future<void> _sizePlatformView() async {
  // ...
  // 进入 resizing 缩放中状态
  _state = _PlatformViewState.resizing;
  
  markNeedsPaint();

  Size targetSize;
  do {
    targetSize = size;
    // 调整原生视图大小,是个异步耗时操作
    await _viewController.setSize(targetSize);
    // 更新 Android 视图大小
    _currentAndroidViewSize = targetSize;
    // We've resized the platform view to targetSize, but it is possible that
    // while we were resizing the render object's size was changed again.
    // In that case we will resize the platform view again.
  } while (size != targetSize);

  _state = _PlatformViewState.ready;
  markNeedsPaint();
}

方法将状态设置为_PlatformViewState.resizing,并标记需要重绘。这是为了告诉Flutter框架,视图的大小正在改变,需要重新绘制视图。

其中包含一个 do...while,这是因为 _viewController.setSize 是一个异步耗时操作,而 Flutter 侧的 AndroidView 视图大小可能处于变化当中。这时,尽管 _viewController.setSize 设置完了,但是 AndroidView 因为处于变化中,所以大小又变了。

于是,设计一个 do...while 环节,让原生视图的大小不断追齐 AndroidView 大小。什么时候停止呢?直到 AndroidView 不动了。举一个例子,假设开发者给 AndroidView 添加了一个缩放动画,在动画执行过程中,上面的 do...while 便不断调整大小,以匹配。直到动画执行结束,AndroidView 大小不再变化了,此时也能从这个循环里出来了。

之后,看到将状态切换为了 _PlatformViewState.ready 状态。

_paintTexture 的 TextureLayer 创建时,有一个 freeze 属性,与 _PlatformViewState.resizing 状态是关联的,让我们结合缩放过程再来理解一下:

do...while 前有一个 markNeedsPaint(); 标记需要绘制,然后便进入了 do...while。假设 resizing 过程特别长,AndroidView 视图大小连续变了假设 10s,这 10s 都落入 do...while 你追我赶。但是 Flutter 的帧调度是正常调度的,在 _sizePlatformView await 释放线程占用后,会进入帧调度,它知道 AndroidView 标记了 NeedsPaint,于是调用 paint,而这时提交的 TextureLayer 里面,只有 rect 变了,由于是 freeze,纹理是不更新的。这说明了什么?假设这个过程中,原生视图是个播放中的播放器,在这个缩放过程中,播放器的视图大小会同步进行拉伸、缩放,但是播放器的内容是不更新的,因为是 freeze。

注:上面这段内容,是我根据代码静态分析后给出的推论,因此可能并不正确。如果你对此有深入了解,欢迎批评指正,谢谢。


本文作者:Maeiee

本文链接:Flutter RenderAndroidView

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!